(function () {
  var colorChooser, AlbumImage, AlbumColors;


  /* colorChooser
   * A series of function that is used to pick up 3 colors among 10 dominating colors
   */
  colorChooser = {
    colorStringToRGBArray: function (colorString) {
      var n, arr = colorString.split(',');

      for (n = 0; n < arr.length; n++) {
        colorString[n] = parseInt(colorString[n], 10);
      }

      return colorString;
    },

    colorDistance: function (colorA, colorB) {
      var colorARGB = colorA,
        colorBRGB = colorB,
        distance = 0,
        n;

      for (n = 0; n < colorARGB.length; n++) {
        distance += (colorARGB[n] - colorBRGB[n]) * (colorARGB[n] - colorBRGB[n]);
      }

      return Math.sqrt(distance);
    },

    getDistances: function (colors) {
      var distances = [],
        c1, c2;

      for (c1 = 0; c1 < colors.length; c1++) {
        distances[c1] = [];
        for (c2 = 0; c2 < colors.length; c2++) {
          distances[c1][c2] = colorChooser.colorDistance(colors[c1], colors[c2]);
        }
      }

      return distances;
    },

    chooseThreeColors: function (colors) {
      var color1 = colors[0],
        colorDistances = colorChooser.getDistances(colors),
        color2Index = 0,
        color3Index = 1, c, c2, color2, color3;

      for (c = 0; c < colors.length; c++) {
        if (colorDistances[0][c] > colorDistances[0][color2Index]) {
          color2Index = c;
        }
      }

      color2 = colors[color2Index];
      color3Index = color2Index + 1;
      if (color3Index >= colors.length) {
        color3Index = 1;
      }

      for (c = 1; c < colors.length; c++) {
        if (c !== color2Index && colorDistances[0][c] > colorDistances[0][color3Index]) {
          color3Index = c;
        }
      }

      color3 = colors[color3Index];

      return [color1, color2, color3];
    }
  };


  /*
   * A Class for the to wrap image,
   * used for counting raw color pixels
   */
  AlbumImage = function (url) {
    this.url = url;
  };

  AlbumImage.prototype.fetch = function (callback) {
    var that = this;

    this.image = new Image();

    this.image.onload = function () {
      if (callback) {
        callback(this);
      }
    };

    this.image.src = this.url;
  };

  AlbumImage.prototype.getCanvas = function () {
    if (this.canvas) {
      return this.canvas;
    }

    var canvas, context;

    canvas = document.createElement('canvas');
    canvas.width = this.image.width;
    canvas.height = this.image.height;
    context = canvas.getContext('2d');

    context.drawImage(this.image, 0, 0);

    return (this.canvas = canvas);
  };

  AlbumImage.prototype.getPixelArray = function () {
    return this.getCanvas().getContext('2d').getImageData(0, 0, this.image.width, this.image.height).data;
  };

  AlbumImage.prototype.getColors = function () {
    if (this.colors) {
      return this.colors;
    }

    var p, colors = [],
      pixelArray = this.getPixelArray();

    for (p = 0; p < pixelArray.length; p += 4) {
      colors.push([pixelArray[p], pixelArray[p + 1], pixelArray[p + 2]]);
    }

    return (this.colors = colors);
  };


  /*
   * AlbumColors
   * Generate pallete among dominating colors
   */
  AlbumColors = function (imageUrl) {
    this.imageUrl = imageUrl;
    this.image = new AlbumImage(imageUrl);
  };

  AlbumColors.prototype.getColors = function (callback) {
    var that = this;
    this.image.fetch(function () {
      that.colors = that.extractMainColors(10);
      if (callback) {
        callback(colorChooser.chooseThreeColors(that.colors));
      }
    });
  };

  AlbumColors.prototype.generateRGBString = function (color) {
    return color.join(',');
  };

  AlbumColors.prototype.getBucket = function (color) {
    // Throw a color into one color bucket
    var bucket = [],
      c;

    for (c = 0; c < color.length; c++) {
      // Naive
      bucket[c] = Math.round(color[c] / 64) * 64;
    }

    return bucket;
  };

  AlbumColors.prototype.getColorsByBucket = function () {
    if (this.colorsByBucket) {
      return this.colorsByBucket;
    }

    var colors, c, color, bucket, colorsByBucket, rgbString;

    colors = this.image.getColors();
    colorsByBucket = {};

    for (c = 0; c < colors.length; c++) {
      color = colors[c];
      bucket = this.getBucket(color);
      rgbString = this.generateRGBString(bucket);

      colorsByBucket[rgbString] = colorsByBucket[rgbString] || [];
      if (colorsByBucket[rgbString]) {
        colorsByBucket[rgbString].push(color);
      }
    }

    return (this.colorsByBucket = colorsByBucket);
  };

  AlbumColors.prototype.getAverageColor = function (colors) {
    var c, r = 0,
      g = 0,
      b = 0;

    for (c = 0; c < colors.length; c++) {
      r += colors[c][0];
      g += colors[c][1];
      b += colors[c][2];
    }

    r = parseInt(r / colors.length, 10);
    g = parseInt(g / colors.length, 10);
    b = parseInt(b / colors.length, 10);

    return [r, g, b];
  };

  AlbumColors.prototype.getColorBuckets = function () {
    if (this.colorBuckets) {
      return this.colorBuckets;
    }

    var colorsByBucket = this.getColorsByBucket(),
      bucket, buckets = [];

    for (bucket in colorsByBucket) {
      if (colorsByBucket.hasOwnProperty(bucket)) {
        buckets.push(bucket);
      }
    }

    return (this.colorBuckets = buckets);
  };

  AlbumColors.prototype.extractMainColors = function (count) {
    if (!this.mainColors) {
      var colorsByBucket = this.getColorsByBucket(),
        colorBuckets = this.getColorBuckets().slice(0),
        b, mainColors = [];

      colorBuckets.sort(function (colorBucketA, colorBucketB) {
        return colorsByBucket[colorBucketB].length - colorsByBucket[colorBucketA].length;
      });

      for (b = 0; b < colorBuckets.length; b++) {
        mainColors.push(this.getAverageColor(colorsByBucket[colorBuckets[b]]));
      }

      this.mainColors = mainColors;
    }

    return this.mainColors.slice(0, count);
  };

  AlbumColors.AlbumImage = AlbumImage;

  window.AlbumColors = AlbumColors;
}());

